Skip to content

feat(spec): adr-0030-notification-event joins CREATION_ATTESTED_MIGRATION_IDS; its docblock states the ledger-claim matrix (#15710 ruling) - #16068

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-15710-notification-event-migration-ledger-claims
Sep 6, 2026
Merged

feat(spec): adr-0030-notification-event joins CREATION_ATTESTED_MIGRATION_IDS; its docblock states the ledger-claim matrix (#15710 ruling)#16068
os-project-manager merged 2 commits into
mainfrom
claude/issue-15710-notification-event-migration-ledger-claims

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15710

Ruling executed

Director seat, comment 5550131736 on #15710 — maintainer verbatim 「同意」 to decision batch #47 item 5, the question batch #21 had reserved (5491064532, item 3: 「注册进台账时其 applied 状态语义要写清」). Quoted verbatim:

  1. Columns. last_run_at is set on every completed non-error run (migrated, already_done, not_applicable); applied_at only on migrated; verified_at is never set by this migration — it has no self-check, and verified_at means a self-check passed. details.outcome carries the four-valued result; an error run writes no ledger claim. The docblock on the constant in packages/spec/src/system/migration.zod.ts states this matrix in place of "silence is not an answer".
  2. Receipt, not gate. Nothing gates on the row; it is what an operator reads, in the shape sys-migration.object.ts already documents for the seed-tenancy repair (verified_at: null, blocking: 0 by construction). A gate would need a self-check that does not exist.
  3. Creation-attested. A datastore created after the cut-over has no legacy inbox rows by construction — the "true by birth, observably" argument the array's own docblock makes for its two members applies verbatim — so the id joins CREATION_ATTESTED_MIGRATION_IDS, and the three live pins (migration-flag.test.ts:137-139, adr0104-attestation-evidence.test.ts) move with it. Leaving it out would make a fresh store's ledger read "never ran" for a registered id, which is false.

Not reopened: #13998 stands on its historical answer. #14025 is not addressed here — PR #15450 landed the registration, and this card was the successor carrier of the question that registration reserved.

What landed

1. The docblock matrix — NOTIFICATION_EVENT_MIGRATION_ID in packages/spec/src/system/migration.zod.ts

The registration-era paragraph ("silence is not an answer") is replaced by the ruled matrix, written as bullets on the constant (no .describe() text changed; symbols and files cited, never line numbers; provenance in one line — batch #47 item 5, the reserved question from batch #21):

column what a run may claim
last_run_at every COMPLETED non-error run — migrated, already_done, not_applicable alike
applied_at only on migrated
verified_at NEVER set by a run — no self-check exists, and verified_at means one passed
blocking 0 by construction
details.outcome the four-valued result, verbatim
an error run writes NO ledger claim

Then "Receipt, not gate" — nothing reads the row as a precondition and nothing may; the shape is the one sys-migration.object.ts documents for the seed-tenancy repair (verified_at: null, blocking: 0), which isDataMigrationFlagVerified answers false to — then the creation-attested paragraph, then one line deferring the run-receipt writer to the runner's lane.

2. CREATION_ATTESTED_MIGRATION_IDS gains NOTIFICATION_EVENT_MIGRATION_ID (third member; existing members and order kept)

The array's docblock now argues the third fact — no legacy sys_notification inbox row here — exactly as it argues the two ADR-0104 facts, and says the attestation row is the same uniform shape as theirs.

⚠️ One reading in the dispatch was corrected against the code, and the docblock states the corrected fact. The dispatch described the fresh store's new row as verified_at: null. attestFreshDatastore (packages/platform-objects/src/system/migration-flag.ts) writes ONE shape for every member — verified_at set at birth, applied_at: null, blocking: 0, details.attested: 'datastore-created-empty' — and the pin the ruling names (migration-flag.test.ts lines 137-141 on main) asserts isDataMigrationVerified is true for every member. So on a fresh store this id's row reads verified BY BIRTH — never by a run, and gating nothing — exactly as the two ADR-0104 rows do. Ruling item 1 governs runs ("never set by this migration"); item 3 puts the id in the array and explicitly moves that pin with it. No runtime writer was changed: a per-id attestation shape would be new contract the ruling does not state (raised as an open question in the report, recommendation: keep the uniform shape).

3. Pins

  • NEW packages/spec/src/system/notification-event-migration-ledger.pin.test.ts (14 cases), in the style of ui/action-requires-confirmation-docblock.pin.test.ts (a source-text docblock pin): the literal three-member list in order; the docblock matrix read as bullets — each column's relation, not its wording — plus the retired sentence absent; a self-test feeding the registration-era paragraph to the same readers; and the arbiter: each run receipt (migrated / already_done / not_applicable, verified_at: null) answers false to isDataMigrationFlagVerified, with a control showing the same row with verified_at set answers true.
  • packages/platform-objects/src/system/migration-flag.test.ts: the by-construction iterations (lines 137-139, 190, 258, 267, 276) needed no edit; one symbol-named case added — the id is attested at birth in the uniform shape — because an iterating pin stays green with the member gone (the ablation below shows exactly that).
  • packages/platform-objects/src/plugin.test.ts: the boot-attestation pins are literal by design — four sorted lists, three toHaveLength(2), and the contradicted-gate case — all moved to three members (adr-0030-notification-event sorts first; the contradicted-gate case now attests the other two ids in array order).
  • packages/objectql/src/adr0104-attestation-evidence.test.ts: measured, NOT moved — it never reads the array (it hand-builds attestation rows for the two ADR-0104 gates and asserts the engine's gate reads, which are keyed to those two ids); 19/19 green unchanged. The ruling's "moves with it" did not hold for this file.

4. Changeset

.changeset/notification-event-migration-ledger-claims.md@objectstack/spec minor, additive: no authorable key, export or accept-set narrows, so the launch-window BREAKING banner does not apply; check:adr-0087-registration judges only declared-breaking changesets (breakingDeclaration: major / **BREAKING / feat!:), so no disposition marker is owed. check:changeset-no-major and check:empty-changeset green.

Runtime side — measured, not changed

The runtime writer of the receipt row is a follow-on card in the runner's lane, filed by the seat on landing. For this PR: no code path writes a sys_migration row for adr-0030-notification-event today (packages/metadata/src/migrations/ has no sys_migration / recordDataMigrationRun / applied_at reference; the three applied_at writers repo-wide are recordDataMigrationRun, attestFreshDatastore and the seed-tenancy backfill, none keyed to this id). No reader gates on it: NOTIFICATION_EVENT_MIGRATION_ID and its literal have zero readers outside packages/spec, and the engine's two ledger gate reads are keyed to the ADR-0104 ids — the stop condition did not trigger. CREATION_ATTESTED_MIGRATION_IDS has exactly one runtime reader, attestFreshDatastore; packages/cli does not read it (the dispatch's M1 was half true).

Verification

Tree 35819dff2 (branch head); spec dist rebuilt from that source (preflight: marker present in 2 built files). Every build/test ran under scripts/pm/os-verify-lock.sh on a shared box; the wrapper's verdict lines are quoted.

  • spec: vitest run src/system/migration.test.ts src/system/notification-event-migration-ledger.pin.test.tsTest Files 2 passed (2), Tests 39 passed (39).
  • platform-objects: vitest run src/plugin.test.ts src/system/migration-flag.test.tsTests 42 passed (42).
  • objectql: vitest run src/adr0104-attestation-evidence.test.ts src/adr0104-lax-deviation-marker.test.tsTests 19 passed (19).
  • Consumer sweep, DOWNSTREAM direction (...@objectstack/spec, the four packages the seat named plus spec itself): turbo run typecheck over spec, platform-objects, objectql, cli, metadata at --concurrency=2Tasks: 61 successful, 61 total (5m40s, shared box; includes spec check:test-typecheck and check:scripts-typecheck, so the new pin file type-checks).
  • spec check:generated✓ All 15 generated artifacts are up to date: no artifact moved (the const already existed; const TSDoc is not part of the generated docs).
  • Gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (5 paths vs merge-base 53cbad9f7): Run reconciliation — 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN, every family exit 0. check:doc-authoring (M4) green in both runs.
  • Ablation — commit first; restore is git checkout HEAD -- path, proven each leg by hash-object == HEAD blob and an empty git diff HEAD; trap on EXIT/INT/TERM; absolute paths:
    • Leg A, member removed (on-disk: member lines 0, blob hash differs), spec rebuilt, ablation-dist-preflight --absent: marker absent from all 217 built files. Spec pin: 2 failed | 12 passed (the literal list; the type/includes case). platform-objects: 9 failed | 33 passed (the 8 literal pins in plugin.test.ts and the new symbol-named case; the iterating loop stayed green — which is why the literal pins exist).
    • Leg B, the verified_at bullet loses its negation (src-only, on-disk proven): spec pin 1 failed | 13 passed, exactly the verified_at case.
    • Final: restored, rebuilt, preflight: marker present in 2 built files, spec pin 14 passed (14), platform-objects 42 passed (42), tree clean.

Declared to CI: turbo ls --affected against 53cbad9f7 lists 75 packages (everything depends on spec); locally this diff can move only the consumer tests over the migration-flag / attestation files and the typecheck of spec's named consumers, which is what ran. The rest of the farm is CI's.

Out of scope

🤖 Generated with Claude Code


Generated by Claude Code

…TION_IDS; docblock states the ledger-claim matrix

The maintainer ruled the three questions the registration reserved:
last_run_at on every completed non-error run, applied_at only on
migrated, verified_at never (no self-check); receipt, not gate; and the
id is creation-attested. The docblock states the matrix in place of
"silence is not an answer"; the array gains the member; the pins that
read the array literally move with it, and a spec pin holds the literal
membership, the docblock relations and the arbiter's answer to the
receipt shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…ation-attested id

The timing and deferral cases in plugin.test.ts assert the attested set
as a sorted literal list or a literal count; both move with the new
member, and the contradicted-gate case now attests the other two ids in
array order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c463d03e07912ffa32f446d8fd03aa32cd406091packageMentionDocs.

Which tree this was computed on

This run read content/docs from 01d03d5f7fd3e71fc77dafa72324683b7094994b — the merge of head 35819dff25579a60a24f6fe3a65f339e0a1ccccd into base c463d03e07912ffa32f446d8fd03aa32cd406091, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 01d03d5f7fd3e71fc77dafa72324683b7094994b && git checkout 01d03d5f7fd3e71fc77dafa72324683b7094994b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c463d03e07912ffa32f446d8fd03aa32cd406091 35819dff25579a60a24f6fe3a65f339e0a1ccccd && git checkout -B drift-repro c463d03e07912ffa32f446d8fd03aa32cd406091 && git merge --no-ff 35819dff25579a60a24f6fe3a65f339e0a1ccccd

node scripts/docs-audit/affected-docs.mjs --json c463d03e07912ffa32f446d8fd03aa32cd406091

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[decision] What may a run of adr-0030-notification-event claim in the sys_migration ledger — which columns, and is it a gate or a receipt?

2 participants